﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using Amazon.DynamoDBv2.Model;
using AmazonDynamoDBStub.Extensions;
using AmazonDynamoDBStub.Helpers;
using AmazonDynamoDBStub.Models;

namespace AmazonDynamoDBStub
{
    static class ExpressionBuilder
    {
        private static Expression<Func<DynamoDBAttribute, bool>> BuildKeyAttributeExpression(KeyValuePair<string, AttributeValue> keyAttribute)
        {
            var parameter = Expression.Parameter(typeof(DynamoDBAttribute), "attr");

            var attributeNameProperty = Expression.Property(parameter, typeof(DynamoDBAttribute).GetProperty("AttributeName"));
            var attributeValueProperty = Expression.Property(parameter, typeof(DynamoDBAttribute).GetProperty("AttributeValue"));
            var attributeTypeProperty = Expression.Property(parameter, typeof(DynamoDBAttribute).GetProperty("AttributeType"));
            var dynamoDBTableProperty = Expression.Property(parameter, typeof(DynamoDBAttribute).GetProperty("DynamoDBTable"));

            var hashKeyProperty = Expression.Property(dynamoDBTableProperty, typeof(DynamoDBTable).GetProperty("HashKey"));
            var rangeKeyProperty = Expression.Property(dynamoDBTableProperty, typeof(DynamoDBTable).GetProperty("RangeKey"));

            var hashKeyAttributeNameProperty = Expression.Property(hashKeyProperty, typeof(DynamoDBKey).GetProperty("AttributeName"));
            var rangeKeyAttributeNameProperty = Expression.Property(rangeKeyProperty, typeof(DynamoDBKey).GetProperty("AttributeName"));

            var hashKeyAttributeNamePredicate = Expression.Equal(attributeNameProperty, hashKeyAttributeNameProperty);
            var rangeKeyAttributeNamePredicate = Expression.Equal(attributeNameProperty, rangeKeyAttributeNameProperty);


            var attributeName = Expression.Constant(keyAttribute.Key);
            var attributeNamePredicate = Expression.Equal(attributeNameProperty, attributeName);
            var attributeValue = Expression.Constant(keyAttribute.Value.GetAttributeValue());
            var attributeValuePredicate = Expression.Equal(attributeValueProperty, attributeValue);
            var attributeType = Expression.Constant(keyAttribute.Value.GetAttributeType());
            var attributeTypePredicate = Expression.Equal(attributeTypeProperty, attributeType);

            var predicateBody = Expression.AndAlso(
                Expression.OrElse(hashKeyAttributeNamePredicate, rangeKeyAttributeNamePredicate),
                Expression.AndAlso(Expression.AndAlso(attributeNamePredicate, attributeTypePredicate), attributeValuePredicate));

            return Expression.Lambda<Func<DynamoDBAttribute, bool>>(predicateBody, parameter);
        }

        public static Expression<Func<IGrouping<Guid, DynamoDBAttribute>, bool>> BuildKeyAttributeExpression(params KeyValuePair<string, AttributeValue>[] keyAttributes)
        {
            var keyAttributeExpressions = keyAttributes.GetValid().Select(BuildKeyAttributeExpression);
            var parameter = Expression.Parameter(typeof(IGrouping<Guid, DynamoDBAttribute>), "row");
            Expression predicateBody = Expression.Constant(true);

            foreach (var expression in keyAttributeExpressions)
            {
                var methodInfo = typeof(Enumerable)
                    .GetMethods(BindingFlags.Static | BindingFlags.Public)
                    .Single(m => m.Name == "Any" && m.GetParameters().Length == 2);
                methodInfo = methodInfo.MakeGenericMethod(typeof(DynamoDBAttribute));

                var predicate = Expression.Call(methodInfo, parameter, expression);
                predicateBody = Expression.AndAlso(predicateBody, predicate);
            }

            return Expression.Lambda<Func<IGrouping<Guid, DynamoDBAttribute>, bool>>(predicateBody, parameter);
        }

        private static Expression<Func<DynamoDBAttribute, bool>> BuildConditionExpression(KeyValuePair<string, Condition> condition)
        {
            var parameter = Expression.Parameter(typeof(DynamoDBAttribute), "attr");

            var attributeNameProperty = Expression.Property(parameter, typeof(DynamoDBAttribute).GetProperty("AttributeName"));
            var attributeValueProperty = Expression.Property(parameter, typeof(DynamoDBAttribute).GetProperty("AttributeValue"));
            var attributeTypeProperty = Expression.Property(parameter, typeof(DynamoDBAttribute).GetProperty("AttributeType"));

            var attributeName = Expression.Constant(condition.Key);
            var attributeNamePredicate = Expression.Equal(attributeNameProperty, attributeName);

            var attributeValue = Expression.Constant(condition.Value.AttributeValueList[0].GetAttributeValue());
            var attributeValueList = Expression.Constant(condition.Value.AttributeValueList.Select(value => value.GetAttributeValue()).ToList());

            Expression attributeValuePredicate;

            switch (condition.Value.ComparisonOperator)
            {
                // Allowed Values: EQ, NE, IN, LE, LT, GE, GT, BETWEEN, NOT_NULL, NULL, CONTAINS, NOT_CONTAINS, BEGINS_WITH
                case "EQ":
                    attributeValuePredicate = Expression.Equal(attributeValueProperty, attributeValue);
                    break;
                case "NE":
                    attributeValuePredicate = Expression.Equal(attributeValueProperty, attributeValue);
                    break;
                case "IN":
                    attributeValuePredicate = Expression.Call(attributeValueList, typeof(List<string>).GetMethod("Contains"), attributeValueProperty);
                    break;
                case "LE":
                    attributeValuePredicate = Expression.LessThanOrEqual(attributeValueProperty, attributeValue);
                    break;
                case "LT":
                    attributeValuePredicate = Expression.LessThan(attributeValueProperty, attributeValue);
                    break;
                case "GE":
                    attributeValuePredicate = Expression.GreaterThanOrEqual(attributeValueProperty, attributeValue);
                    break;
                case "GT":
                    attributeValuePredicate = Expression.GreaterThan(attributeValueProperty, attributeValue);
                    break;
                case "BETWEEN":
                    var attributeValue2 = Expression.Constant(condition.Value.AttributeValueList[1].GetAttributeValue());
                    var first = Expression.GreaterThanOrEqual(attributeValueProperty, attributeValue);
                    var second = Expression.LessThanOrEqual(attributeValueProperty, attributeValue2);
                    attributeValuePredicate = Expression.And(first, second);
                    break;
                case "BEGINS_WITH":
                    var methodInfo = typeof(string).GetMethods(BindingFlags.Instance | BindingFlags.Public).Single(m => m.Name == "StartsWith" && m.GetParameters().Length == 1);
                    attributeValuePredicate = Expression.Call(attributeValueProperty, methodInfo, attributeValue);
                    break;
                default:
                    ExceptionHelper.ThrowAmazonDynamoDBException();
                    return null;
            }

            var attributeType = Expression.Constant(condition.Value.AttributeValueList[0].GetAttributeType());
            var attributeTypePredicate = Expression.Equal(attributeTypeProperty, attributeType);

            var predicateBody = Expression.And(Expression.And(attributeNamePredicate, attributeTypePredicate), attributeValuePredicate);

            return Expression.Lambda<Func<DynamoDBAttribute, bool>>(predicateBody, parameter);
        }

        public static Expression<Func<IGrouping<Guid, DynamoDBAttribute>, bool>> BuildConditionExpression(params KeyValuePair<string, Condition>[] conditions)
        {
            var conditionExpressions = conditions.GetValid().Select(BuildConditionExpression);
            var parameter = Expression.Parameter(typeof(IGrouping<Guid, DynamoDBAttribute>), "row");
            Expression predicateBody = Expression.Constant(true);

            foreach (var expression in conditionExpressions)
            {
                var methodInfo = typeof(Enumerable)
                    .GetMethods(BindingFlags.Static | BindingFlags.Public)
                    .Single(m => m.Name == "Any" && m.GetParameters().Length == 2);
                methodInfo = methodInfo.MakeGenericMethod(typeof(DynamoDBAttribute));

                var predicate = Expression.Call(methodInfo, parameter, expression);
                predicateBody = Expression.AndAlso(predicateBody, predicate);
            }

            return Expression.Lambda<Func<IGrouping<Guid, DynamoDBAttribute>, bool>>(predicateBody, parameter);
        }
    }
}
